Developer control for the control-plane IP family policy - #339
Merged
Conversation
api.bringyour.com and connect.bringyour.com publish AAAA records in Hurricane Electric space that some ISPs route badly. Happy Eyeballs self-heals a pre-connect blackhole, but an HE tunnel that passes the small SYN/SYN-ACK and then drops the large TLS ServerHello makes IPv6 win the race and stall after connecting. The sdk now demotes a family proven to fail that way; this adds the Developer Tools row (Auto / Force IPv4 / Force IPv6) that surfaces and overrides it on iOS. - IpFamily: pure policy-to-label/clamp/cycle helpers, mirroring the sdk's IpFamilyPolicy constants. - IpFamilyState: the device/networkSpace/process-global three-way write path (matching android), with an in-flight guard around the write-then-read-back so a second tap can't be silently lost. - DeveloperView: a "Control connections" row that works with no device and with the tunnel down, since that's when it's needed. - PacketTunnelProvider: records the known limitation that the first connect after a tunnel-down force can still dial under Auto, and why that's an acceptable, bounded cost rather than a bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vN7zh8WeQYCtirhcA3aWw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The iOS half of the control-plane IPv4/IPv6 policy. Depends on urnetwork/sdk#152, which depends on urnetwork/connect#207.
Why
Some users' ISPs route badly to the service's Hurricane Electric IPv6 address. The path connects and then stalls in TLS — invisible to Happy Eyeballs, which races only the TCP handshake. connect learns and routes around it; this is the manual override and the visibility.
What this adds
A tri-state Control connections row in the Developer screen, cycling Automatic → Force IPv4 → Force IPv6, modelled on the log-verbosity row beside it including its read-back discipline: the value shown is what the SDK reports, never what was last tapped, so a set that was clamped or refused is visible rather than assumed.
One thing it deliberately does not copy from that row. The verbosity row is inert without a device — there is nothing to set a level on. This row does the opposite: it reads and writes the process-global policy, so it works signed out and with the tunnel down. Those are exactly the states a user is in when the API is unreachable, which is the only reason to reach for it. A device-gated row would be inert in the situation it exists to rescue.
IpFamily.swiftis pure vocabulary — labels, detail text, clamp, cycle order — so it is testable with no device.IpFamilyState.swiftholds the state and the one write path, which prefers the device (reaching the extension over RPC when the tunnel is up) and falls back to the network space, then the process-global setter.The detail line reports what Automatic has learned, e.g. "Automatic — IPv6 demoted for 4m (2 strikes)". That status comes from
device?.getControlIpFamilyStatus(), not the app process: with the tunnel up the extension is the process that dials and therefore the one that learns, so an app-local read is blind precisely where the heuristic fires.Integration note
PacketTunnelProvider.swifthad moved upstream since the fork base. Our change there is comment-only — aKNOWN LIMITATIONblock recording why the extension's first connect after a force can still dial under Auto (the extension restores from its own container, and the app's first sync arrives ~0.5–1.5 s after the RPC listener opens). Integrated alongside your changes, nothing clobbered.That limitation was assessed and deliberately not fixed: the demotion ledger is in-memory with no persistence, so a fresh extension process starts empty on every tunnel start and an unforced user re-learns the bad family each connect. Closing the window would buy one repetition of a cost the unforced path pays every time, at the price of a second, staleable source of truth beside
LocalState.Verification
xcodebuild build(generic/platform=iOS) and thenetworkTestssuite both pass —** BUILD SUCCEEDED **/** TEST SUCCEEDED **, exit 0.Also installed and exercised on a physical iPhone (Release, iOS 27): the row is live signed out and with the tunnel down, cycles correctly, and a force survives a full app restart.
urnetwork/sdkcheckout, becauseWidgetSnapshotWriter.swift(an upstream-only file) referencesSdkGetColorHexandSdkOrderConnectedProviderLocations, which a stale sibling SDK build lacks.🤖 Generated with Claude Code
https://claude.ai/code/session_014vN7zh8WeQYCtirhcA3aWw